/**
 * 类似于protobuffer，协议格式为json，更符合js的使用
 * 
 * base64相关处理借鉴于
 * @see https://github.com/protobufjs/protobuf.js/tree/master/lib/base64
 * float相关处理借鉴于
 * @see https://github.com/protobufjs/protobuf.js/tree/master/lib/float
 * utf8相关处理借鉴于
 * @see https://github.com/protobufjs/protobuf.js/tree/master/lib/utf8
 * 
 * 
 * @author zp
 * @version 2.0.0
 */

/**
 * [注] nodejs环境下通过Uint8Array将ArrayBuffer和Buffer互相转换
 * @example
 * // Buffer ---> ArrayBuffer
 * function toArrayBuffer(buf) {
 *     var ab = new ArrayBuffer(buf.length);
 *     var view = new Uint8Array(ab);
 *     for (var i = 0; i < buf.length; ++i) {
 *         view[i] = buf[i];
 *     }
 *     return ab;
 * }
 * // ArrayBuffer ---> Buffer
 * function toBuffer(ab) {
 *     var buf = new Buffer(ab.byteLength);
 *     var view = new Uint8Array(ab);
 *     for (var i = 0; i < buf.length; ++i) {
 *         buf[i] = view[i];
 *     }
 *     return buf;
 * }
 */

/**
 * @example
 * var { registerProto, Type, encode, decode, singleArray } = binary;
 * registerProto(1001, [
 *     ['name', Type.String],
 *     ['age', Type.Uint],
 *     ['sex', Type.Uint]
 * ])
 * registerProto(1002, [
 *     ['info', 1001],
 *     ['gold', Type.Uint],
 *     ['items', [Type.Uint, Type.String]]
 * ])
 * registerProto(1003, [
 *     ['array0', Type.Array],
 *     ['array1', singleArray(1002)],
 *     ['array2', singleArray([1001, 1002])],
 *     ['array3', singleArray(Type.Uint)],
 *     ['array4', singleArray([Type.Uint, Type.String])]
 * ])

 * var buffer = encode({ name: 'Mary', age: 18, sex: 0 }, 1001);
 * decode(buffer);

 * var buffer = encode({ info: { name: 'Mary', age: 18, sex: 0 }, gold: 10, array: [100, 2, 3] }, 1002);
 * decode(buffer);

 * var buffer = encode({
 *     array0: ['你好啊','我很好👍'],
 *     array1: [{ info: { name: 'James', age: 30, sex: 1 }, gold: 10, array: [100, 2, 3] }],
 *     array2: [[{}, { info: { name: 'Mary', age: 18, sex: 0 }, gold: 10, array: [100, 2, 3] }]],
 *     array3: [568],
 *     array4: [[0, '零'], [1, '一'], [2, '二'], [3, '三']]
 * }, 1003);
 * decode(buffer);
 */

/**
 * https://segmentfault.com/a/1190000014533505 中提到如果服务器开启了压缩了话，需要进行解压操作，并推荐了pako.js
 * (具体也不知道这个压缩怎么回事，测试的时候也没遇到这个问题，先写注释记下来)
 * @see https://github.com/nodeca/pako/edit/master/dist/pako.js
 * @example
 * let compressdata = new Uint8Array(buffer, byteOff, length);
 * let uncompress = pako.inflate(compressdata);//解压数据
 * let uncompressdata = uncompress.buffer;// ArrayBuffer {}
 * let dataViewData = new DataView(uncompressdata, 0);//解压后数据
 */

/**
* A minimal base64 implementation for number arrays.
* @memberof util
* @namespace
*/

import * as core from './module/binary-core';

type TypeKey = 'Int32' | 'Uint32' | 'Int64' | 'Uint64' | 'Boolean' | 'String' | 'Base64' | 'Array' | 'Object' | 'Int' | 'Uint' | 'Float' | 'Double';
type TypeValue = 'int32' | 'uint32' | 'int64' | 'uint64' | 'boolean' | 'string' | 'base64' | 'array' | 'object' | 'float' | 'double';

type ProtoID = number;
type ProtoType = TypeValue;

type ProtoBase = ProtoID | ProtoType;
type ProtoObject = [string, ProtoBase][];
type ProtoArray = ProtoBase[];
type ProtoAll = ProtoBase | ProtoObject | ProtoArray;

type ProtoSingleArray = { singleArray: ProtoAll };
type ProtoSimple = ProtoBase | ProtoBase[] | ProtoSingleArray | ProtoSingleArray[];

type Proto = ProtoSimple | [string, ProtoSimple][];

export class Length {
    static int32(data: any) {
        if (!core.isNumber(data)) data = 0;
        return core.int32.length(data);
    }

    static uint32(data: any) {
        if (!core.isNumber(data)) data = 0;
        return core.uint32.length(data);
    }

    static int64(data: any) {
        if (!core.isNumber(data)) data = 0;
        return core.int64.length(data);
    }

    static uint64(data: any) {
        if (!core.isNumber(data)) data = 0;
        return core.uint64.length(data);
    }

    static float(data: any) {
        if (!core.isNumber(data)) data = 0;
        return core.float.length(data);
    }

    static double(data: any) {
        if (!core.isNumber(data)) data = 0;
        return core.double.length(data);
    }

    static boolean(data: any) {
        return core.boolean.length(!!data);
    }

    static string(data: any) {
        if (!core.isString(data)) data = '';
        return core.string.length(data);
    }

    static base64(data: any) {
        if (!core.isBase64(data)) data = '';
        return core.base64.length(data);
    }

    static array(data: any) {
        if (!core.isArray(data)) data = [];
        return core.array.length(data);
    }

    static object(data: any) {
        if (!core.isMap(data)) data = {};
        return core.object.length(data);
    }
}

export class Encode {
    public buffer: ArrayBuffer = null;
    private view: Uint8Array = null;
    private index: number = 0;

    constructor(length: number) {
        this.buffer = new ArrayBuffer(length);
        this.view = new Uint8Array(this.buffer);
        this.index = 0;
    }

    int32(data: any) {
        if (!core.isNumber(data)) data = 0;

        core.int32.encode(data, this.view, this.index);
        this.index += core.int32.bytes;
    }

    uint32(data: any) {
        if (!core.isNumber(data)) data = 0;

        core.uint32.encode(data, this.view, this.index);
        this.index += core.uint32.bytes;
    }

    int64(data: any) {
        if (!core.isNumber(data)) data = 0;

        core.int64.encode(data, this.view, this.index);
        this.index += core.int64.bytes;
    }

    uint64(data: any) {
        if (!core.isNumber(data)) data = 0;

        core.uint64.encode(data, this.view, this.index);
        this.index += core.uint64.bytes;
    }

    float(data: any) {
        if (!core.isNumber(data)) data = 0;

        core.float.encode(data, this.view, this.index);
        this.index += core.float.bytes;
    }

    double(data: any) {
        if (!core.isNumber(data)) data = 0;

        core.double.encode(data, this.view, this.index);
        this.index += core.double.bytes;
    }

    boolean(data: any) {
        core.boolean.encode(!!data, this.view, this.index);
        this.index += core.boolean.bytes;
    }

    string(data: any) {
        if (!core.isString(data)) data = '';

        core.string.encode(data, this.view, this.index);
        this.index += core.string.bytes;
    }

    base64(data: any) {
        if (!core.isBase64(data)) data = '';

        core.base64.encode(data, this.view, this.index);
        this.index += core.base64.bytes;
    }

    array(data: any) {
        if (!core.isArray(data)) data = [];

        core.array.encode(data, this.view, this.index);
        this.index += core.array.bytes;
    }

    object(data: any) {
        if (!core.isMap(data)) data = {};

        core.object.encode(data, this.view, this.index);
        this.index += core.object.bytes;
    }
}

export class Decode {
    private view: Uint8Array = null;
    private index: number = 0;

    constructor(buffer: ArrayBuffer) {
        this.view = new Uint8Array(buffer);
        this.index = 0;
    }

    int32() {
        const value = core.int32.decode(this.view, this.index);
        this.index += core.int32.bytes;
        return value;
    }

    int64() {
        const value = core.int64.decode(this.view, this.index);
        this.index += core.int64.bytes;
        return value;
    }

    uint32() {
        const value = core.uint32.decode(this.view, this.index);
        this.index += core.uint32.bytes;
        return value;
    }

    uint64() {
        const value = core.uint64.decode(this.view, this.index);
        this.index += core.uint64.bytes;
        return value;
    }

    float() {
        const value = core.float.decode(this.view, this.index);
        this.index += core.float.bytes;
        return value;
    }

    double() {
        const value = core.double.decode(this.view, this.index);
        this.index += core.double.bytes;
        return value;
    }

    boolean() {
        const value = core.boolean.decode(this.view, this.index);
        this.index += core.boolean.bytes;
        return value;
    }

    string() {
        const value = core.string.decode(this.view, this.index);
        this.index += core.string.bytes;
        return value;
    }

    base64() {
        const value = core.base64.decode(this.view, this.index);
        this.index += core.base64.bytes;
        return value;
    }

    array() {
        const value = core.array.decode(this.view, this.index);
        this.index += core.array.bytes;
        return value;
    }

    object() {
        const value = core.object.decode(this.view, this.index);
        this.index += core.object.bytes;
        return value;
    }
}

/**
 * 通过protoID得到proto
 * @param protoID 
 */
function getProtoByID(protoID: ProtoID): Proto {
    return protoCache[protoID] || null;
}

/**
 * 将protoID转为proto
 */
function realProto(param: Proto): Proto {
    if (isProtoID(param)) {
        return getProtoByID(param as ProtoID);
    }
    return param;
}

function isProtoID(param: any): boolean {
    return core.isInteger(param) && param >= 0;
}

function isProtoType(param: any): boolean {
    return typeof param === 'string' && !!TypeReverse[param];
}

function isProto(proto: any): boolean {
    if (isProtoID(proto)) return true;
    if (isProtoType(proto)) return true;
    if (isSingleArray(proto)) return true;
    if (isProtoObject(proto)) return true;
    if (isProtoArray(proto)) return true;

    return false;
}

function isSingleArray(param: any): boolean {
    if (!core.isMap(param)) return false;
    return param.hasOwnProperty('singleArray');
}

function isProtoObject(param: any): boolean {
    if (!(param instanceof Array)) return false;
    if (param.length === 0) return false;
    return param.every(function (value) {
        if (!(value instanceof Array)) return false;
        if (value.length !== 2) return false;
        return typeof value[0] === 'string' && isProto(value[1]);
    })
}

function isProtoArray(param: any): boolean {
    if (!(param instanceof Array)) return false;
    if (param.length === 0) return false;
    return param.every(function (value) {
        return isProto(value);
    })
}

/**
 * 获得singleArray的类型
 * @param param 
 */
function singleArrayProto(param: ProtoSingleArray): Proto {
    return realProto(param.singleArray);
}

/**
 * 标记单一类型的数组
 * @param proto 
 */
export function SingleArray(proto: ProtoAll): ProtoSingleArray {
    return { singleArray: proto };
}

function DataLen(data: any, proto: Proto) {
    proto = realProto(proto);

    let length = 0;

    if (isProtoObject(proto)) {
        if (!core.isMap(data)) data = {};
        (proto as ProtoObject).forEach(function ([key, proto]) {
            length += DataLen(data[key], proto);
        })
    } else if (isProtoArray(proto)) {
        if (!core.isArray(data)) data = [];
        (proto as ProtoArray).forEach(function (proto, index) {
            length += DataLen(data[index], proto);
        })
    } else if (isSingleArray(proto)) {
        // 如果是SingleArray的话，开头有N字节记录数组长度
        if (!core.isArray(data)) data = [];
        length += core.uint32.length(data.length);
        proto = singleArrayProto(proto as ProtoSingleArray);
        (data as any[]).forEach(function (value) {
            length += DataLen(value, proto);
        })
    } else if (isProtoType(proto)) {
        length += Length[proto as string](data);
    } else {
        throw new Error("DataLen: 'proto' is bad");
    }

    return length;
}

function encodeData(encode: Encode, data: any, proto: Proto) {
    proto = realProto(proto);

    if (isProtoObject(proto)) {
        if (!core.isMap(data)) data = {};
        (proto as ProtoObject).forEach(function ([key, proto]) {
            encodeData(encode, data[key], proto);
        })
    } else if (isProtoArray(proto)) {
        if (!core.isArray(data)) data = [];
        (proto as ProtoArray).forEach(function (proto, index) {
            encodeData(encode, data[index], proto);
        })
    } else if (isSingleArray(proto)) {
        if (!core.isArray(data)) data = [];
        encode.uint32(data.length);
        proto = singleArrayProto(proto as ProtoSingleArray);
        (data as any[]).forEach(function (value) {
            encodeData(encode, value, proto);
        })
    } else if (isProtoType(proto)) {
        encode[proto as string](data);
    } else {
        throw new Error("encodeData: 'proto' is bad");
    }
}

function decodeData(decode: Decode, proto: Proto) {
    proto = realProto(proto);

    if (isProtoObject(proto)) {
        const obj = {};
        (proto as ProtoObject).forEach(function ([key, proto]) {
            obj[key] = decodeData(decode, proto);
        })
        return obj;
    } else if (isProtoArray(proto)) {
        return (proto as ProtoArray).map(function (proto) {
            return decodeData(decode, proto);
        })
    } else if (isSingleArray(proto)) {
        const arr = [];
        const len = decode.uint32();
        proto = singleArrayProto(proto as ProtoSingleArray);
        for (let index = 0; index < len; index++) {
            arr.push(decodeData(decode, proto));
        }
        return arr;
    } else if (isProtoType(proto)) {
        return decode[proto as string]();
    } else {
        throw new Error("decodeData: 'proto' is bad");
    }
}

// const TypeByte = {
//     'Int8': 1,
//     'Uint8': 1,
//     'Int16': 2,
//     'Uint16': 2,
//     'Int32': 4,
//     'Uint32': 4,
//     'Float32': 4,
//     'Float64': 8,
//     'BigInt64': 8,
//     'BigUint64': 8
// }

// export const Type = {
//     'Int8': 'Int8',                 // 1byte  -128 to 127
//     'Uint8': 'Uint8',               // 1byte  0 to 255
//     'Uint8Clamped': 'Uint8',        // 1byte  0 to 255
//     'Int16': 'Int16',               // 2byte  -32768 to 32767
//     'Uint16': 'Uint16',             // 2byte  0 to 65535
//     'Int32': 'Int32',               // 4byte  -2147483648 to 2147483647
//     'Uint32': 'Uint32',             // 4byte  0 to 4294967295
//     'Float32': 'Float32',           // 4byte  1.2x10^-38 to 3.4x10^38
//     'Float64': 'Float64',           // 8byte  5.0x10^-324 to 1.8x10^308
//     'BigInt64': 'BigInt64',         // 8byte  -2^63 to (2^63)-1
//     'BigUint64': 'BigUint64',       // 8byte  0 to (2^64)-1
// }

const TypeReverse: { [key in TypeValue]: TypeKey } = {
    'int32': 'Int32',
    'uint32': 'Uint32',
    'int64': 'Int64',
    'uint64': 'Uint64',
    'boolean': 'Boolean',
    'string': 'String',
    'base64': 'Base64',
    'array': 'Array',
    'object': 'Object',
    'float': 'Float',
    'double': 'Double'
}

export const Type: { [key in TypeKey]: TypeValue } = {
    'Int32': 'int32',               // 4byte  -2147483648 to 2147483647
    'Uint32': 'uint32',             // 4byte  0 to 4294967295
    'Int64': 'int64',               // 4byte  -2147483648 to 2147483647
    'Uint64': 'uint64',             // 4byte  0 to 4294967295
    'Boolean': 'boolean',           // 1byte  0 to 255
    'String': 'string',             // 1byte  0 to 255
    'Base64': 'base64',             // 1byte  0 to 255
    'Array': 'array',               // 1byte  0 to 255
    'Object': 'object',             // 1byte  0 to 255
    'Int': 'int32',
    'Uint': 'uint32',
    'Float': 'float',
    'Double': 'double'
}

/**
 * 序列化
 * 开头2字节用来存储proto的id
 */
export function encode(data: any, id: ProtoID) {
    const proto = protoCache[id];

    if (!proto) throw new Error(`encode error: 'id' is bad. ${id}`);

    if (handleCache[id]) return handleCache[id][0](data, proto);

    const len = DataLen(data, proto);
    const encode = new Encode(len + core.uint32.length(Number(id)));
    encode.uint32(Number(id));
    encodeData(encode, data, proto);
    return encode.buffer;
}

/**
 * 反序列化
 * 开头2字节代表proto的id
 */
export function decode(buffer: ArrayBuffer) {
    const decode = new Decode(buffer);
    const id = decode.uint32();
    const proto = protoCache[id];

    if (!proto) throw new Error("decode error: 'buffer' is bad");

    if (handleCache[id]) return handleCache[id][1](buffer, proto);

    return decodeData(decode, proto);
}

type EncodeHandle = (data: any, proto: Proto) => ArrayBuffer;
type DecodeHandle = (buffer: ArrayBuffer, proto: Proto) => any;

/**
 * proto缓存
 */
const protoCache: { [key in ProtoID]: Proto } = Object.create(null);
const handleCache: { [key in ProtoID]: [EncodeHandle, DecodeHandle] } = Object.create(null);

/**
 * 注册proto
 * id: 必须是个正整数(或正整数字符串)
 * proto: 符合Map的初始化结构，即[['b',1],['a',2]]
 */
export function registerProto(protos: { [id in string | number]: Proto }): void;
export function registerProto(id: number | string, proto: Proto, encode?: EncodeHandle, decode?: DecodeHandle): void;
export function registerProto(...args) {
    if (args.length === 1) {
        const protos = args[0];
        for (let id in protos) {
            registerProto(id, protos[id]);
        }
    } else {
        const id = Number(args[0]);
        const proto = args[1];

        if (!core.isInteger(id) || id < 0) throw new Error(`registerProto error: 'id' or 'proto' is bad. ${id} - ${proto}`);

        if (encode && decode) handleCache[id] = [encode, decode];
        protoCache[id] = proto;
    }
}

/**
 * 注销proto
 * @param id 
 */
export function deregisterProto(id: number | string) {
    delete protoCache[id];
    delete handleCache[id];
}

/**
 * 将proto转成json格式
 */
export function protoToJson() {
    return JSON.stringify(protoCache);
}

// window['binary'] = exports;

// registerProto(1003, [
//     ['array0', Type.Array],
//     ['array1', 1001],
//     ['array2', SingleArray(1002)],
//     ['array3', SingleArray([Type.Uint, 1002])],
//     ['array4', SingleArray([Type.Uint, Type.String])]
// ], function () {

// })